/* Originally based on the javac task from apache-ant-1.7.1.
 * The license in that file is as follows:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or
 *   more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information regarding
 *   copyright ownership.  The ASF licenses this file to You under
 *   the Apache License, Version 2.0 (the "License"); you may not
 *   use this file except in compliance with the License.  You may
 *   obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing,
 *   software distributed under the License is distributed on an "AS
 *   IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 *   express or implied.  See the License for the specific language
 *   governing permissions and limitations under the License.
 *
 */

/*
 * Copyright Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the authors tag. All rights reserved.
 */
package org.eclipse.ceylon.ant;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.eclipse.ceylon.common.Constants;

@ToolEquivalent("compile")
@AntDoc("To compile the module `com.example.foo` whose source code is in the\n"+ 
        "`src` directory to a module repository in the `build` directory, with\n"+ 
        "verbose compiler messages:\n"+
        "\n"+
        "<!-- lang: xml -->\n"+
        "    <target name=\"compile\" depends=\"ceylon-ant-taskdefs\">\n"+
        "      <ceylon-compile src=\"src\" out=\"build\" verbose=\"true\">\n"+
        "        <module name=\"com.example.foo\"/>\n"+
        "      </ceylon-compile>\n"+
        "    </target>\n")
public class CeylonCompileAntTask extends LazyCeylonAntTask  {

    static final String FAIL_MSG = "Compile failed; see the compiler error output for details.";

    @AntDoc("For example:\n\n"
            + "<!-- lang: xml -->\n"
            + "    <javac key=\"-encoding\">UTF-8</javac>\n\n"
            + "or\n\n"
            + "<!-- lang: xml -->\n"
            + "    <javac key=\"-encoding\" value=\"UTF-8\"/>>\n")
    public static class JavacOption {
        String key;
        String value;
        
        @AntDoc("The name of the `javac` option")
        @Required
        public void setKey(String key) {
            this.key = key;
        }

        @AntDoc("The value of the `javac` option. "
                + "Required when the corresponding `javac` option requires an argument")
        public void setValue(String value) {
            this.value = value;
        }

        public void addText(String value) {
            this.value = value;
        }
    }
    
    @AntDoc("Suppresses compiler warnings. Warnings can be suppressed by "
            + "type, for example\n"
            + "\n"
            + "<!-- lang: xml -->\n"
            + "    <suppressWarning>filenameNonAscii</suppressWarning>\n"
            + "\n"
            + "or eqiuvalently \n"
            + "\n"
            + "<!-- lang: xml -->\n"
            + "    <suppressWarning value=\"filenameNonAscii\">\n"
            + "\n"
            + "or all warnings can be suppressed by not naming any "
            + "specific warnings:\n"
            + "\n"
            + "<!-- lang: xml -->\n"
            + "    <suppressWarning/>\n"
            + "\n")
    public static class SuppressWarning {
        String value;
        
        @AntDoc("The name of the warning(s) to be suppressed.")
        public void setValue(String value) {
            this.value = value;
        }

        @AntDocIgnore
        public void addText(String value) {
            this.value = value;
        }
    }
    
    private static final FileFilter ARTIFACT_FILTER = new FileFilter() {
        @Override
        public boolean accept(File pathname) {
            String name = pathname.getName();
            return name.endsWith(".car")
                    || name.endsWith(".src")
                    || name.endsWith(".car.sha1")
                    || name.endsWith(".src.sha1");
        }
    };
    
    private Path res;
    private Path classpath;
    private final ModuleSet moduleSet = new ModuleSet();
    private FileSet files;
    private List<JavacOption> javacOptions = new ArrayList<JavacOption>(0);
    private Boolean noOsgi;
    private String osgiProvidedBundles;
    private Boolean jigsaw;
    private Boolean noPom;
    private Boolean pack200;
    private List<SuppressWarning> suppressWarnings = new ArrayList<SuppressWarning>(0);
    private boolean suppressAllWarnings = false;
    private String target;
    
    private List<File> compileList = new ArrayList<File>(2);
    private Set<Module> modules = null;
    
    public CeylonCompileAntTask() {
        super("compile");
    }

    /**
     * Set to true to disable OSGi manifest declaration in the META-INF/MANIFEST.MF car file.
     */
    @OptionEquivalent
    public void setNoOsgi(Boolean noOsgi) {
        this.noOsgi = noOsgi;
    }
    
    public boolean getNoOsgi() {
        return noOsgi;
    }

    /**
     * Set to true to enable Java 9 (Jigsaw) module-info.class generation in the car file.
     */
    @OptionEquivalent
    public void setGenerateModuleInfo(Boolean jigsaw) {
        this.jigsaw = jigsaw;
    }
    
    public boolean getGenerateModuleInfo() {
        return jigsaw;
    }

    /**
     * Comma-separated list of module names.
     * The listed modules are expected to be OSGI bundles provided by the framework,
     * and will be omitted from the 'Required-Bundle' OSGI header in the
     * manifest of the generated car file.
     */
    @OptionEquivalent
    public void setOsgiProvidedBundles(String osgiProvidedBundles) {
        this.osgiProvidedBundles = osgiProvidedBundles;
    }
    
    public String getOsgiProvidedBundles() {
        return osgiProvidedBundles;
    }

    /**
     * Set to true to disable Maven POM module declaration in the META-INF/maven/ car folder.
     */
    @OptionEquivalent
    public void setNoPom(Boolean noPom) {
        this.noPom = noPom;
    }
    
    public boolean getNoPom() {
        return noPom;
    }

    public Boolean getPack200() {
        return pack200;
    }
    
    public String getTarget() {
        return target;
    }
    
    @OptionEquivalent
    public void setTarget(String target) {
        this.target = target;
    }
    
    /**
     * Set to true to enable repacking the generated car file using pack200.
     */
    @OptionEquivalent("--pack200")
    public void setPack200(Boolean pack200) {
        this.pack200 = pack200;
    }

    @OptionEquivalent
    public void addConfiguredSuppressWarning(SuppressWarning sw) {
        this.suppressWarnings.add(sw);
        if (sw.value == null || sw.value.isEmpty()) {
            suppressAllWarnings = true;
        }
    }

    /**
     * Set the resource directories to find the resource files.
     * @param res the resource directories as a path
     */
    @OptionEquivalent("--resource")
    public void setResource(Path res) {
        if (this.res == null) {
            this.res = res;
        } else {
            this.res.append(res);
        }
    }

    @OptionEquivalent("--resource")
    public void addConfiguredResource(Src res) {
        Path p = new Path(getProject(), res.value);
        if (this.res == null) {
            this.res = p;
        } else {
            this.res.append(p);
        }
    }

    public List<File> getResource() {
        if (this.res == null) {
            return Collections.singletonList(getProject().resolveFile(Constants.DEFAULT_RESOURCE_DIR));
        }
        String[] paths = this.res.list();
        ArrayList<File> result = new ArrayList<File>(paths.length);
        for (String path : paths) {
            result.add(getProject().resolveFile(path));
        }
        return result;
    }

    /**
     * Sets the classpath
     * @param path
     */
    @AntDocIgnore
    public void setClasspath(Path path){
        if(this.classpath == null)
            this.classpath = path;
        else
            this.classpath.add(path);
    }
    
    /**
     * Adds a &lt;classpath&gt; nested param
     */
    public Path createClasspath(){
        if(this.classpath == null)
            return this.classpath = new Path(getProject());
        else
            return this.classpath.createPath(); 
    }
    
    /**
     * Sets the classpath by a path reference
     * @param classpathReference
     */
    @AntDocIgnore
    public void setClasspathref(Reference classpathReference) {
        createClasspath().setRefid(classpathReference);
    }

    @AntDoc("Modules to be compiled")
    public void addConfiguredModuleSet(ModuleSet moduleset) {
        this.moduleSet.addConfiguredModuleSet(moduleset);
    }
    
    /**
     * Adds a module to compile
     * @param module the module name to compile
     */
    @AntDoc("A module to be compiled")
    public void addConfiguredModule(Module module) {
        this.moduleSet.addConfiguredModule(module);
    }
    
    @AntDoc("Modules to be compiled")
    public void addConfiguredSourceModules(SourceModules sourceModules) {
        this.moduleSet.addConfiguredSourceModules(sourceModules);
    }
    
    @AntDoc("A `<fileset>` containing Ceylon and Java files to be compiled (incremental compilation)")
    public void addFiles(FileSet fileset) {
        if (this.files != null) {
            throw new BuildException("<ceylonc> only supports a single <files> element");
        }
        this.files = fileset;
    }

    /** Adds an option to be passed to javac via a {@code --javac=...} option */
    @OptionEquivalent("--javac")
    public void addConfiguredJavacOption(JavacOption javacOption) {
        this.javacOptions.add(javacOption);
    }
    
    /**
     * Clear the list of files to be compiled and copied..
     */
    protected void resetFileLists() {
        compileList.clear();
    }
    
    /**
     * Check that all required attributes have been set and nothing silly has
     * been entered.
     * 
     * @exception BuildException if an error occurs
     */
    protected void checkParameters() throws BuildException {
        if (this.moduleSet.getModules().isEmpty()
                && this.files == null) {
            throw new BuildException("You must specify a <module>, <moduleset> and/or <files>");
        }
    }
    
    @Override
    protected Commandline buildCommandline() {
        resetFileLists();
        if (files != null) {
            addToCompileList(getSrc());
            addToCompileList(getResource());
        }
        modules = moduleSet.getModules();
        
        if (compileList.size() == 0 && modules.size() == 0){
            log("Nothing to compile");
            return null;
        }
     
        LazyHelper lazyTask = new LazyHelper(this) {
            @Override
            protected File getArtifactDir(Module module) {
                File outModuleDir = new File(getOut(), module.toVersionedDir().getPath());
                return outModuleDir;
            }
            
            @Override
            protected FileFilter getArtifactFilter() {
                return ARTIFACT_FILTER;
            }

            @Override
            protected long getOldestArtifactTime(File file) {
                long mtime = Long.MAX_VALUE;
                String name = file.getPath().toLowerCase();
                if (name.endsWith(".car") || name.endsWith(".src")) {
                    JarFile jarFile = null;
                    try {
                        jarFile = new JarFile(file);
                        Enumeration<JarEntry> entries = jarFile.entries();
                        while(entries.hasMoreElements()){
                            JarEntry entry = entries.nextElement();
                            if (entry.getTime() < mtime) {
                                mtime = entry.getTime();
                            }
                        }
                    } catch (Exception ex) {
                        // Maybe something's wrong with the CAR so let's return MIN_VALUE
                        mtime = Long.MIN_VALUE;
                    } finally {
                        if (jarFile != null) {
                            try {
                                jarFile.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        }
                    }
                } else {
                    mtime = file.lastModified();
                }
                return mtime;
            }
            
            @Override
            protected long getArtifactFileTime(Module module, File file) {
                File moduleDir = getArtifactDir(module);
                String name = module.getName() + ((module.getVersion() != null) ? "-" + module.getVersion() : "") ;
                File carFile = new File(moduleDir, name + ".car");
                File srcFile = new File(moduleDir, name + ".src");
                long carTime = getCarEntryTime(carFile, file);
                long srcTime = getZipEntryTime(srcFile, file);
                return Math.min(carTime, srcTime);
            }

            private long getCarEntryTime(File carFile, File entryFile) {
                long mtime = Long.MAX_VALUE;
                String name = entryFile.getPath().replace('\\', '/');
                Properties mapping = readMappingFromCar(carFile);
                if (mapping != null) {
                    JarFile jarFile = null;
                    try {
                        jarFile = new JarFile(carFile);
                        for (String className : mapping.stringPropertyNames()) {
                            String srcName = mapping.getProperty(className);
                            if (name.equals(srcName) || name.endsWith("/" + srcName)) {
                                ZipEntry entry = jarFile.getEntry(className);
                                if (entry != null) {
                                    mtime = Math.min(mtime, entry.getTime());
                                }
                            }
                        }
                    } catch (Exception ex) {
                        // Ignore
                    } finally {
                        if (jarFile != null) {
                            try {
                                jarFile.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        }
                    }
                }
                return mtime;
            }
            
            private long getZipEntryTime(File zipFile, File entryFile) {
                if (zipFile.isFile()) {
                    String name = entryFile.getPath().replace('\\', '/');
                    JarFile jarFile = null;
                    try {
                        jarFile = new JarFile(zipFile);
                        Enumeration<JarEntry> entries = jarFile.entries();
                        while(entries.hasMoreElements()){
                            JarEntry entry = entries.nextElement();
                            if (name.equals(entry.getName()) || name.endsWith("/" + entry.getName())) {
                                return entry.getTime();
                            }
                        }
                    } catch (Exception ex) {
                        // Ignore
                    } finally {
                        if (jarFile != null) {
                            try {
                                jarFile.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        }
                    }
                }
                return Long.MAX_VALUE;
            }
            
            private Properties readMappingFromCar(File carFile) {
                if (carFile.isFile()) {
                    JarFile jarFile = null;
                    try {
                        jarFile = new JarFile(carFile);
                        ZipEntry entry = jarFile.getEntry("META-INF/mapping.txt");
                        if (entry != null) {
                            InputStream inputStream = jarFile.getInputStream(entry);
                            try {
                                Properties mapping = new Properties();
                                mapping.load(inputStream);
                                return mapping;
                            } finally {
                                inputStream.close();
                            }
                        }
                    } catch (Exception ex) {
                        // Ignore
                    } finally {
                        if (jarFile != null) {
                            try {
                                jarFile.close();
                            } catch (IOException e) {
                                // Ignore
                            }
                        }
                    }
                }
                return null;
            }
        };
        
        if (lazyTask.filterFiles(compileList) 
                && lazyTask.filterModules(modules)) {
            log("Everything's up to date");
            return null;
        }
        return super.buildCommandline();
    }
    
    private void addToCompileList(List<File> dirs) {
        for (File srcDir : dirs) {
            if (srcDir.isDirectory()) {
                FileSet fs = (FileSet)this.files.clone();
                fs.setDir(srcDir);
                
                DirectoryScanner ds = fs.getDirectoryScanner(getProject());
                String[] files = ds.getIncludedFiles();

                for(String fileName : files)
                    compileList.add(new File(srcDir, fileName));
            }
        }
    }
    
    /**
     * Perform the compilation.
     */
    @Override
    protected void completeCommandline(Commandline cmd) {
        super.completeCommandline(cmd);
        
        for (JavacOption opt : javacOptions) {
            String arg = (opt.key != null) ? opt.key + ":" + opt.value : opt.value;
            appendOptionArgument(cmd, "--javac", arg);
        }
        
        for (File res : getResource()) {
            appendOptionArgument(cmd, "--resource", res.getAbsolutePath());
        }
        
        if (noOsgi != null && noOsgi.booleanValue())
            appendOption(cmd, "--no-osgi");

        if (jigsaw != null && jigsaw.booleanValue())
            appendOption(cmd, "--generate-module-info");

        if (osgiProvidedBundles != null && !osgiProvidedBundles.isEmpty())
            appendOptionArgument(cmd, "--osgi-provided-bundles", osgiProvidedBundles);

        if (noPom != null && noPom.booleanValue())
            appendOption(cmd, "--no-pom");

        if (pack200!= null && pack200.booleanValue())
            appendOption(cmd, "--pack200");
        
        if (target != null && !target.isEmpty()) {
            appendOptionArgument(cmd, "--target", target);
        }
        
        if (suppressWarnings != null) {
            if (suppressAllWarnings) {
                appendOption(cmd, "--suppress-warning");
            } else {
                for (SuppressWarning sw : suppressWarnings) {
                    appendOption(cmd, "--suppress-warning=" + sw.value);
                }
            }
        }
        
        if(classpath != null){
            throw new RuntimeException("-classpath not longer supported");
            /*String path = classpath.toString();
            cmd.createArgument().setValue("-classpath");
            cmd.createArgument().setValue(Util.quoteParameter(path));*/
        }
        // files to compile
        for (File file : compileList) {
            log("Adding source file: "+file.getAbsolutePath(), Project.MSG_VERBOSE);
            cmd.createArgument().setValue(file.getAbsolutePath());
        }
        // modules to compile
        for (Module module : modules) {
            log("Adding module: "+module, Project.MSG_VERBOSE);
            cmd.createArgument().setValue(module.toVersionlessSpec());
        }
    }

    @Override
    protected String getFailMessage() {
        return FAIL_MSG;
    }
}
